// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;

namespace SnapObjects.Data
{
    internal abstract class StatefulModel<TModel> : IStatefulModel
    {
        protected readonly TModel _originalModel;
        protected readonly TModel _currentModel;
        protected readonly IModelFactory<TModel> _modelFactory;
        protected readonly IModelAccessor<TModel> _accessor;
        protected readonly ICompositeModelAccessor<TModel> _compositeModelAccessor;

        private readonly int _propertyCount;
        private readonly IPropertiesStateTracker _statusTracker;

        protected ModelState _modelState;

        // Used for entry track
        protected StatefulModel(
            ICompositeModelAccessor<TModel> compositeModelAccessor,
            IModelFactory<TModel> modelFactory,
            ModelState modelState,
            ChangeTrackingStrategy changeTrackingStrategy)
        {
            _compositeModelAccessor = compositeModelAccessor;
            _modelFactory = modelFactory;
            _accessor = modelFactory.GetAccessor();

            this.ChangeTrackingStrategy = changeTrackingStrategy;

            _propertyCount = _accessor.ModelMeta.PropertyCount;
            _statusTracker = new PropertiesStateTracker(_propertyCount);

            _originalModel = modelFactory.Create();
            _currentModel = modelFactory.Create();

            _modelState = modelState;
        }

        //Used for TrackUpdate(TModel, TModel)
        protected StatefulModel(
            ICompositeModelAccessor<TModel> compositeModelAccessor,
            TModel originalModel,
            TModel currentModel,
            ChangeTrackingStrategy changeTrackingStrategy)
        {
            _compositeModelAccessor = compositeModelAccessor;
            _modelFactory = compositeModelAccessor.ModelFactory;
            _accessor = _modelFactory.GetAccessor();

            this.ChangeTrackingStrategy = changeTrackingStrategy;

            _propertyCount = _accessor.ModelMeta.PropertyCount;
            _statusTracker = new PropertiesStateTracker(_propertyCount);

            _originalModel = originalModel;
            _currentModel = currentModel;

            _modelState = ModelState.Modified;
        }

        protected StatefulModel(
            ICompositeModelAccessor<TModel> compositeModelAccessor,
            TModel model,
            ModelState modelState,
            ChangeTrackingStrategy changeTrackingStrategy)
            : this(compositeModelAccessor.ModelFactory, model, modelState, changeTrackingStrategy)
        {
            _compositeModelAccessor = compositeModelAccessor;
        }

        protected StatefulModel(
            IModelFactory<TModel> modelFactory,
            TModel model,
            ModelState modelState,
            ChangeTrackingStrategy changeTrackingStrategy)
        {
            _modelFactory = modelFactory;
            _accessor = modelFactory.GetAccessor();

            this.ChangeTrackingStrategy = changeTrackingStrategy;

            if (modelState == ModelState.New ||
                modelState == ModelState.NewModified)
            {
                _originalModel = modelFactory.Create();
            }
            else
            {
                _originalModel = _accessor.Clone(model);
            }

            _currentModel = model;

            _modelState = modelState;
            _propertyCount = _accessor.ModelMeta.PropertyCount;
            _statusTracker = new PropertiesStateTracker(_propertyCount);

            if (modelState == ModelState.NewModified)
            {
                this.MarkModifiedPropertiesModified();
            }
        }

        protected virtual bool IsValueEqual(IPropertyMeta property)
        {
            return ModelHelper.IsValueEquals(
                this.GetOriginalValue(property.Name),
                this.GetCurrentValue(property.Name));
        }

        protected virtual bool AnyValueChanged(bool onlyCheckKeyProperty = false)
        {
            var modelMeta = _accessor.ModelMeta;

            var properties = onlyCheckKeyProperty
                ? modelMeta.UpdatableProperties.Where(m => m.IsKey)
                : modelMeta.UpdatableProperties;

            return properties.Any(x => !this.IsValueEqual(x));
        }

        public virtual void PerformSetValueOperations(IEmbeddedStateTracker embeddedStateTracker)
        {
            if (_accessor.ModelMeta is IPocoModelMeta pocoModelMeta)
            {
                if (pocoModelMeta.HasSetValueProperties)
                {
                    foreach (var property in pocoModelMeta.SetValueProperties)
                    {
                        this.PerformSetValueOperations(property, embeddedStateTracker);
                    }
                }
            }
        }

        private void PerformSetValueOperations(
            IPocoProperty propertyMeta,
            IEmbeddedStateTracker embeddedStateTracker)
        {
            if (_compositeModelAccessor == null ||
                !propertyMeta.HasSetValueOperation)
            {
                return;
            }

            var setValueOperations = propertyMeta.SetValueOperations;

            foreach (var setValue in setValueOperations)
            {
                var sourceProperty = setValue.Source;
                var targetProperty = setValue.Target;

                if (setValue.Source.StartsWith("$"))
                {
                    sourceProperty = setValue.Source.Substring(1);
                }

                if (setValue.Target.StartsWith("$"))
                {
                    targetProperty = setValue.Target.Substring(1);
                }

                var sourceValue = this.GetCurrentValue(sourceProperty);

                if (setValue.SetValueStrategy == SetValueStrategy.Always ||
                    (setValue.SetValueStrategy == SetValueStrategy.AfterParentCreated &&
                        _modelState == ModelState.NewModified) ||
                    (setValue.SetValueStrategy == SetValueStrategy.AfterParentUpdated &&
                        _modelState == ModelState.Modified))
                {
                    if (_compositeModelAccessor is ICompositeModelAccessor accessor)
                    {
                        accessor.SetEmbeddedValue(
                            embeddedStateTracker,
                            setValue.ModelSelector,
                            _currentModel,
                            propertyMeta,
                            targetProperty,
                            sourceValue);
                    }
                }
            }
        }

        public virtual void ApplyDefaultValue(TModel model)
        {
            foreach (var property in _accessor.ModelMeta.Properties)
            {
                if (property is IPocoProperty poco &&
                    poco.Attribute.DefaultValueDefined &&
                    this.GetPropertyState(property.Index) == PropertyState.NotModified)
                {
                    _accessor.SetValue(model, property.Index, poco.Attribute.DefaultValue);

                    // TODO: do for?
                    _accessor.SetValue(_originalModel, property.Index, poco.Attribute.DefaultValue);
                }
            }
        }

        public bool CheckedOut { get; set; }
        public object Current => _currentModel;
        public ChangeTrackingStrategy ChangeTrackingStrategy { get; private set; }
        public object Original => _originalModel;

        public ModelState ModelState
        {
            get => _modelState;
            set
            {
                if (!this.SetModelStatus(value))
                {
                    throw new InvalidOperationException("Cannot set ModeSate to " + value.ToString());
                }
            }
        }

        public ModelState LatestModelState
        {
            get
            {
                if (this.CheckedOut)
                {
                    if (this.ModelState == ModelState.NotModified ||
                        this.ModelState == ModelState.NotTracked)
                    {
                        return this.AnyValueChanged() ? ModelState.Modified : this.ModelState;
                    }
                    else if (this.ModelState == ModelState.New)
                    {
                        return this.AnyValueChanged() ? ModelState.NewModified : this.ModelState;
                    }
                    else
                    {
                        return this.ModelState;
                    }
                }
                else
                {
                    if (this.ModelState == ModelState.Deleted)
                    {
                        return ModelState.Deleted;
                    }

                    if (this.ChangeTrackingStrategy == ChangeTrackingStrategy.Snapshot && this.AnyValueChanged())
                    {
                        return this.ModelState == ModelState.New || this.ModelState == ModelState.NewModified ?
                            ModelState.NewModified : ModelState.Modified;
                    }
                    else if (this.ChangeTrackingStrategy == ChangeTrackingStrategy.PropertyState)
                    {
                        return this.ModelState;
                    }

                    // The state of the model is newmodifed when adding model data using the add data method. 
                    if (this.ModelState == ModelState.NewModified)
                    {
                        return ModelState.NewModified;
                    }

                    return ModelState.NotModified;
                }
            }
        }

        public virtual void SetIdentityValue(string name, object value)
        {
            this.SetCurrentValue(name, value);
        }

        public void SetCurrentValue(int index, object value)
        {
            _accessor.SetValue(_currentModel, index, value);
        }

        public void SetCurrentValue(string name, object value)
        {
            _accessor.SetValue(_currentModel, name, value);
        }

        public object GetOriginalValue(int index)
        {
            return _accessor.GetValue(_originalModel, index);
        }

        public virtual object GetOriginalValue(string name)
        {
            return _accessor.GetValue(_originalModel, name);
        }

        public object GetOriginalValue(Func<object, object> selector)
        {
            return selector(_originalModel);
        }

        public object GetCurrentValue(int index)
        {
            return _accessor.GetValue(_currentModel, index);
        }

        public virtual object GetCurrentValue(string name)
        {
            return _accessor.GetValue(_currentModel, name);
        }

        public bool IsValueModified(IPropertyMeta property)
        {
            if (this.CheckedOut)
            { 
                return !this.IsValueEqual(property) || _statusTracker.IsModified(property.Index);
            }
            
            if (this.ChangeTrackingStrategy == ChangeTrackingStrategy.Snapshot)
            {
                return !this.IsValueEqual(property);
            }

            if (this.ChangeTrackingStrategy == ChangeTrackingStrategy.PropertyState)
            {
                return _statusTracker.IsModified(property.Index);
            }

            return false;
        }

        public PropertyState GetPropertyState(int index)
        {
            if (this.CheckedOut)
            {
                return (!this.IsValueEqual(index) || _statusTracker.IsModified(index))
                    ? PropertyState.Modified : PropertyState.NotModified;
            }
            
            if (this.ChangeTrackingStrategy == ChangeTrackingStrategy.Snapshot)
            {
                return !this.IsValueEqual(index) ? PropertyState.Modified : PropertyState.NotModified;
            }

            if (this.ChangeTrackingStrategy == ChangeTrackingStrategy.PropertyState)
            {
                return _statusTracker.IsModified(index) ? PropertyState.Modified : PropertyState.NotModified;
            }

            return PropertyState.NotModified;
        }

        public void SetPropertyState(int index, PropertyState propertyState)
        {
            if (propertyState == PropertyState.Modified)
            {
                _statusTracker.MarkModified(index);
            }
            else
            {
                _statusTracker.MarkNotModified(index);
            }
        }

        public bool HasModifiedProperty()
        {
            if (this.CheckedOut)
            { 
                return this.AnyValueChanged() || _statusTracker.HasModified;
            }
            
            if (this.ChangeTrackingStrategy == ChangeTrackingStrategy.Snapshot)
            {
                return this.AnyValueChanged();
            }

            if (this.ChangeTrackingStrategy == ChangeTrackingStrategy.PropertyState)
            {
                return _statusTracker.HasModified;
            }

            return false;
        }

        public bool HasModifiedKey()
        {
            return this.AnyValueChanged(true);
        }

        public void ResetPropertyState(PropertyState propertyState)
        {
            if (propertyState == PropertyState.Modified)
            {
                _statusTracker.MarkAllModified();
            }
            else
            {
                _statusTracker.MarkAllNotModified();
            }
        }

        public virtual void ChangeModelStateToDelete()
        {
            _modelState = ModelState.Deleted;
        }

        public void ChangeModelStateFromDelete(ModelState modelState)
        {
            _modelState = modelState;
        }

        private void MarkModifiedPropertiesModified()
        {
            var properties = _accessor.ModelMeta.Properties;

            foreach (var property in properties)
            {
                if (!this.IsValueEqual(property))
                {
                    this.SetPropertyState(property.Index, PropertyState.Modified);
                }
            }
        }

        private bool IsValueEqual(int index)
        {
            return ModelHelper.IsValueEquals(
                this.GetOriginalValue(index),
                this.GetCurrentValue(index));
        }

        private bool SetModelStatus(ModelState modelState)
        {

            if (_modelState == modelState)
            {
                return true;
            }

            if (_modelState == ModelState.New)
            {
                if (modelState == ModelState.NewModified ||
                    modelState == ModelState.Modified)
                {
                    _modelState = modelState;
                }
                else
                {
                    return false;
                }

                return true;
            }
            else if (_modelState == ModelState.NewModified)
            {
                if (modelState == ModelState.Modified)
                {
                    _modelState = ModelState.Modified;
                }
                else if (modelState == ModelState.NotModified)
                {
                    _modelState = ModelState.New;
                }
                else
                {
                    return false;
                }

                return true;
            }
            else if (_modelState == ModelState.Modified)
            {
                if (modelState == ModelState.New)
                {
                    _modelState = ModelState.NewModified;
                }
                else if (modelState == ModelState.NewModified ||
                    modelState == ModelState.NotModified)
                {
                    _modelState = modelState;
                }
                else
                {
                    return false;
                }

                return true;
            }
            else if (_modelState == ModelState.NotModified ||
                _modelState == ModelState.NotTracked)
            {
                if (modelState == ModelState.New ||
                    modelState == ModelState.NewModified ||
                    modelState == ModelState.Modified)
                {
                    _modelState = modelState;
                }
                else
                {
                    return false;
                }

                return true;
            }

            return false;
        }
    }
}
